﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Toom.CellularAutomata.Debug;
using Toom.CellularAutomata.Common;


namespace Toom.CellularAutomata.Model
{
	public class CAField
	{
		// has:
		// Dimensions
		//   axis name
		//   unit vector name
		//   boundaries config
		// Automata States
		// Rules
		// FieldInit

		public int valMin, valMax;
		public int sizeX, sizeY;
		public int time;

		public CAFieldChunkManager fieldCM;

		#region CTOR
		public CAField(int sizeX, int sizeY, int valMin, int valMax )
		{
			this.valMin = valMin;
			this.valMax = valMax;

			this.sizeX = sizeX;
			this.sizeY = sizeY;

			this.time = 0;

			this.fieldCM = new CAFieldChunkManager( this );
		}
		#endregion


		#region Get and Set DataAt
		public int GetDataAt(CAPoint p)
		{
			return fieldCM.GetDataAt( p.x, p.y );
		}

		public int GetDataAt(int x, int y)
		{
			return fieldCM.GetDataAt( x, y );
		}

		public void SetDataAt(int x, int y, int newData)
		{
			// checking validity of data
			if ( newData<valMin || newData>valMax )
				throw new Exception( "CAField.SetDataAt: newData="+newData+" is outside of range ["+valMin+","+valMax+"] allowed for this field " );

			fieldCM.SetDataAt( x, y, newData );
		}

		#endregion


		public bool IsAllZero()
		{
			for ( int y = 0 ; y < sizeY ; y++ )
				for ( int x = 0 ; x < sizeX ; x++ )
					if ( GetDataAt( x, y ) != 0 )
						return false;
			return true;
		}

		#region DoTransform
		public void DoTransform( CAGameType whichRule, CAOptimization opt )
		{
			CARuleStatic rule;

			if (whichRule == CAGameType.CONWAY)
				rule = new CARuleStaticSumEightNeighborsConway(this);
			else if ( whichRule == CAGameType.NEC_MAX )
				rule = new CARuleStaticMaximumNEC( this );
			else if ( whichRule == CAGameType.NEC_MIN )
				rule = new CARuleStaticMinimumNEC( this );
			else if ( whichRule == CAGameType.NEC_MEDIAN )
				rule = new CARuleStaticMedianNEC( this );
			else if ( whichRule == CAGameType.SWAP_ZEROS_AND_TWOS )
				rule = new CARuleStaticSwapTwosAndZeros( this );
			else
				throw new Exception("Unknown Rule, cannot continue");


			// ask field chunk manager for list of rectangles
			List<CARectangle> useful = fieldCM.GetUsefulArea( opt );


			//System.Threading.Tasks.Parallel...
			// Parallel.ForEach()
			// Parallel.For( 0, 100, delegate( int i ) { a[i] = a[i]*a[i]; } );


			if ( CASettings.UseMultiCPU )
			{
				// multi-CPU run
				
				//Parallel.For( 0, useful.Count(), delegate( int uid )
				for ( int uid=0 ; uid < useful.Count() ; uid++ )
				{
					CARectangle rect = useful[uid];

					//for ( int y = rect.top ; y < ( rect.top+rect.height ) ; y++ )
					int y0 = rect.top;
					int yMax = rect.top+rect.height;
					if ( yMax > sizeY ) yMax=sizeY;

					Parallel.For( y0, yMax, delegate( int y ) 
					{
						int x0 = rect.left;
						int xMax = rect.left + rect.width;
						if ( xMax > sizeX ) xMax=sizeX;
						
						for ( int x = x0 ; x < xMax ; x++ )
						{
							int newVal = rule.CalculateTransform( x, y );
							fieldCM.SetNewDataAt( x, y, newVal );
						}
					} );
				} 
			}
			else
			{
				// single CPU run
				for ( int uid=0 ; uid < useful.Count() ; uid++ )
				{
					CARectangle rect = useful[uid];

					for ( int y = rect.top ; y < ( rect.top+rect.height ) ; y++ )
					{
						if ( y >= sizeY ) continue;
						for ( int x = rect.left ; x < ( rect.left+rect.width ) ; x++ )
						{
							if ( x >= sizeX ) continue;

							int newVal = rule.CalculateTransform( x, y );
							fieldCM.SetNewDataAt( x, y, newVal );
						}
					}
				}

			}



			fieldCM.GotoNewTime();
			time++;
		}

		public void DoTransform(CARule rule, CAOptimization opt )
		{
			List<CARectangle> useful = fieldCM.GetUsefulArea( opt );

			foreach ( CARectangle rect in useful )
			{
				for ( int y = rect.top ; y < ( rect.top+rect.height ) ; y++ )
					for ( int x = rect.left ; x < ( rect.left+rect.width ) ; x++ )
					{
						int newVal = rule.Evaluate( this, x, y );
						fieldCM.SetDataAt( x, y, newVal );
					}
			}

			fieldCM.GotoNewTime();
			time++;
		}
		#endregion



		#region Init functions

		public void InitEmpty()
		{
			// nothing to do here :-)
		}

		public void InitRandom( int min, int max )
		{
			if ( min < valMin || max > valMax )
				throw new Exception( "CAField.InitRandom cannot process values smaller or larger than how field was setup." );

			Random r = new Random(System.DateTime.Now.Millisecond);

			for (int y = 0; y < sizeY; y++)
				for (int x = 0; x < sizeX; x++)
					//SetDataAt(x, y] = r.Next(max-min+1) + min;
					SetDataAt(x,y, r.Next(max-min+1) + min);
		}

		public void InitBar( int min, int max )
		{
			if ( min < valMin || max > valMax )
				throw new Exception( "CAField.InitRandom cannot process values smaller or larger than how field was setup." );

			int nrColors = max - min;
			int nrUnits = 1 + 2 * nrColors;
			float dUnit = (float)sizeY / nrUnits;

			for ( int i = 0; i<nrColors; i++)
			{
				for ( int y=(int)(dUnit*( 1+2*i )) ; y<dUnit*( 2+2*i ) ; y++ )
					for ( int x=sizeX/4 ; x<=sizeX*3/4 ; x++ )
						SetDataAt( x, y, i+min+1 );
			}
		}


		public void InitDiamond( int min, int max )
		{
			if ( min < valMin || max > valMax )
				throw new Exception( "CAField.InitRandom cannot process values smaller or larger than how field was setup." );

			// first determine if horizontal or vert is smaller. use smaller dimension as a guide.
			int sizeMax = sizeY;
			int shiftY = 0;
			if (sizeX < sizeMax)
			{
				sizeMax = sizeX;
				shiftY = (sizeY - sizeX) / 2;
			}

			// calc number of colors/CAValues, and therefore number of divisions
			int nrColors = max-min+1;

			int d = ( sizeY / 2 ) / ( nrColors );
			// draw 1's
			int xMid    = sizeX / 2;

			int kd=1;
			for ( int color=min+1 ; color<=max ; color++,kd++ )
			{
				int yBottom =           d*kd + shiftY;
				int yTop    = sizeMax - d*kd + shiftY;

				int x1, x2, y1=0, y2=0;

				for ( int dy = 0 ; ; dy++ )
				{
					y1 = yTop - dy;
					y2 = yBottom + dy;

					if ( y1 < y2 )
						break;

					for ( int dx = 0 ; dx <= dy ; dx++ )
					{
						x1 = xMid + dx;
						x2 = xMid - dx;
						SetDataAt( x1, y1, color );
						SetDataAt( x1, y2, color );
						SetDataAt( x2, y1, color );
						SetDataAt( x2, y2, color );
					}
				}
			}
		
		}//method InitDiamond()


		public void InitCircle( int min, int max )
		{
			// first determine if horizontal or vert is smaller. use smaller dimansion as a guide.
			int sizeMax = sizeY;
			int shiftY = 0;
			if (sizeX < sizeMax)
			{
				sizeMax = sizeX;
				shiftY = (sizeY - sizeX) / 2;
			}

			int nrColors = max-min+1;

			int d = ( sizeY / 2 ) / ( nrColors );
			// draw 1's
			int xMid    = sizeX / 2;
			double yMid = sizeY / 2;

			int kd=1;
			for ( int color=min+1 ; color<=max ; color++, kd++ )
			{
				int yBottom =           d*kd + shiftY;
				int yTop    = sizeMax - d*kd + shiftY;

				double x1, x2, y1 = 0, y2 = 0;
				double hyp = yTop-yMid;

				for ( int dy = 0 ; ; dy++ )
				{
					y1 = yTop - dy;
					y2 = yBottom + dy;

					if ( y1 < y2 )
						break;

					// do math
					double lv = (double)y1 - yMid;
					double angle = Math.Asin( lv / hyp );
					double lh = Math.Cos( angle );

					int dxMax = (int)( lh*hyp );

					for ( double dx = 0 ; dx <= dxMax ; dx++ )
					{
						x1 = xMid + dx;
						x2 = xMid - dx;
						SetDataAt( (int)x1, (int)y1, color );
						SetDataAt( (int)x1, (int)y2, color );
						SetDataAt( (int)x2, (int)y1, color );
						SetDataAt( (int)x2, (int)y2, color );
					}
				}

			}

		}//method InitCircle()


		public void InitFillWithVerticalStripes(int min, int max)
		{
			int nrColors = ( max-min ) +1;
			float dx = (float)sizeX / nrColors;

			int counter=0;
			for ( int color=min ; color<=max ; color++,counter++ )
				for ( int x=(int)(counter*dx); x<(counter+1)*dx; x++ )
					if ( x< sizeX )
						for ( int y=0 ; y<sizeY ; y++ )
							SetDataAt( x, y, color );


		}//method FillWithVerticalStripes()


		#endregion

		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			string spacing = "";

			sb.AppendLine( GetTitle() );

			for (int y = sizeY-1; y >= 0; y--)
			{
				for (int x = 0; x < sizeX; x++)
				{
					int d = GetDataAt( x, y );
					char ds;

					if ( d < 0 )
						ds = '?';
					else
						ds = (char)(d + 48);
				
					sb.Append(ds);
					sb.Append(spacing);
				}
				sb.AppendLine();
			}

			return sb.ToString();
		}//ToString


		public string GetTitle()
		{
			return String.Format("Field at t={0}, IsAllZero={1}", time, IsAllZero() );
		}




		//void ParMatrixMult( int size, double[,] m1, double[,] m2, double[,] result ) 
		//{ 
		//	Parallel.For( 
		//		0, 
		//		size, 
		//		delegate( int i ) 
		//		{ 
		//			for ( int j = 0 ; j < size ; j++ ) 
		//			{ 
		//				result[i, j] = 0; 
		//				for ( int k = 0 ; k < size ; k++ ) 
		//				{ 
		//					result[i, j] += m1[i, k] * m2[k, j]; 
		//				} 
		//			} 
		//		} 
		//	); 
		//}



	}
}
